Skip to content

Add configurable futures roll date#9538

Open
scarab-systems wants to merge 1 commit into
QuantConnect:masterfrom
scarab-systems:feature-9440-configurable-futures-roll
Open

Add configurable futures roll date#9538
scarab-systems wants to merge 1 commit into
QuantConnect:masterfrom
scarab-systems:feature-9440-configurable-futures-roll

Conversation

@scarab-systems

Copy link
Copy Markdown

Summary

  • Adds DataMappingMode.TradingDaysBeforeExpiry for continuous futures that need to roll before the normal last-trading-day mapping date.
  • Threads a tradeable-day roll offset and optional contract month cycle through continuous future subscriptions, universe settings, history requests, mapping events, and the Python wrapper.
  • Reuses existing LastTradingDay map-file rows for the new mapping mode and applies the optional contract month cycle when walking continuous future contract depth.

Motivation

Fixes #9440.

The issue describes two related continuous futures needs:

  • rolling a configurable number of trading days before the normal last-trading-day roll;
  • restricting held futures contracts to selected contract months, for example skipping weak open-interest months.

Testing

  • dotnet build Tests/QuantConnect.Tests.csproj --no-restore -clp:ErrorsOnly --verbosity quiet

Added focused tests for:

  • TradingDaysBeforeExpiry reusing LastTradingDay map-file rows;
  • contract-depth walking with a selected contract month cycle;
  • skipping a current mapped contract when its month is outside the selected cycle;
  • adding tradeable days across closed exchange dates.

I was able to build the test project locally. Direct local NUnit execution in my Linux container aborted during global Python/test-host initialization before the selected tests ran, so CI should be the authoritative full test run.

Adds a TradingDaysBeforeExpiry mapping mode for continuous futures and threads the tradeable-day offset and contract month cycle through subscription, history, universe, mapping-event, and Python wrapper paths.

Reuses LastTradingDay map-file rows for the new mode and applies the optional contract month cycle when walking continuous future contract depth.

Adds coverage for LastTradingDay row reuse, contract-month-cycle depth walking, and tradeable-day offset handling.

Verification: dotnet build Tests/QuantConnect.Tests.csproj --no-restore -clp:ErrorsOnly --verbosity quiet
@brandon-68

brandon-68 commented Jul 4, 2026

Copy link
Copy Markdown

Dear All,

To provide some context, I actually reached out to Alex from QC team via QC ticket and gave him the draft specs for the proposed changes before he raised #9440, after which i provided a very comprehensive follow-up comment in #9440 detailing the proposed changes. So, I hope that #9538 would eventually land.

Coming back to #9538, the mapping-side design looks right to me: shifting the map-file search date forward by the tradeable-day offset, applied consistently in both ContinuousContractUniverse (drives the traded contract) and MappingEventProvider (drives data splicing), and the month-cycle walk in AdjustSymbolByOffset keeps SubscriptionDataConfig.MappedSymbol coherent since the getter returns the adjusted underlying's ID.

However, price normalization was not threaded through, and the failure is silent:

  1. Common/Data/Auxiliary/MapFile.cs:108 aliases TradingDaysBeforeExpiryLastTradingDay for mapping, but there is no equivalent for factors: Common/Data/Auxiliary/MappingContractFactorProvider.cs:66,75,84 match factor rows with row.DataMappingMode == dataMappingMode.
  2. Factor files only contain rows for modes 0–3. This is verifiable in the repo's own shipped test data: every row in Data/future/cme/factor_files/es.csv carries "DataMappingMode":0/1/2/3. With mode 4, no row ever matches and GetPriceFactor falls through to the default — 1 for BackwardsRatio, 0 for the Panama modes.
  3. Both scaling paths pass the config's mapping mode: Engine/DataFeeds/SubscriptionUtils.cs:132 and Engine/DataFeeds/Enumerators/PriceScaleFactorEnumerator.cs:112.
  4. BackwardsRatio is the default normalization for futures (Common/Extensions.cs:3973), so any user adding a continuous future with the new mode gets an unadjusted stitched series with roll gaps — no error, no warning. Every indicator and backtest statistic computed on it would be contaminated.

Note that simply aliasing to LastTradingDay factor rows would not be correct either: with rolls shifted N tradeable days earlier, the splice factor must be computed at the shifted roll date, and since N is a free user parameter, factors cannot be precomputed per (mode, N). Correct support seems to require either on-the-fly factor computation at mapping events from the two contracts' prices, or changes to the data pipeline — I'd welcome the QC team's view on which is preferred.

Suggestions to make this mergeable:

  • Short term / fail fast: in MappingContractFactorProvider.GetPriceFactor, throw or at minimum log a warning when the mode is TradingDaysBeforeExpiry and an adjusted normalization mode is requested, rather than silently returning the default factor.
  • Regression algorithms: the repo convention for continuous-futures behavior changes (cf. ContinuousFutureRegressionAlgorithm.cs) is C#/Python regression algorithms with pinned statistics. I'm attaching a skeleton that asserts (a) rolls occur exactly dataMappingModeDaysOffset tradeable days early, (b) the newly mapped contract has more than the offset remaining, and (c) the BackwardsRatio series is genuinely price-adjusted across rolls — assertion (c) will currently fail and is the detector for the gap above.
  • Secondary: MappingEventProvider.Initialize now calls MarketHoursDatabase.FromDataFolder().GetEntry(...) unconditionally for every mapped subscription, equities included; consider resolving lazily (only when the mode is TradingDaysBeforeExpiry) to avoid a new hard MHDB dependency on previously working subscriptions.

Happy to help test. This PR implements #9440, currently among the top-voted items on the QC roadmap.

ContinuousFutureTradingDaysBeforeExpiryRegressionAlgorithm.cs

@brandon-68

Copy link
Copy Markdown

Local test results confirming the review above (commit 44003ff)

Environment: Windows, .NET SDK v 10.0.301, LEAN v2.5.0.0, fresh clone with pull/9538/head checked out, repo-shipped sample data only. Unit tests were not run locally (Tests/AssemblyInitialize.cs initializes an embedded Python runtime for the whole assembly regardless of test filter, requiring the foundation-container Python 3.11 environment — possibly the same wall the PR description mentions hitting); deferring unit test validation to CI.

Method: matched A/B backtest using the regression algorithm below (ES continuous contract, 2013-07-01 → 2014-01-01, BackwardsRatio, monthly trading). The two runs are identical except for the mapping mode; the algorithm asserts (1) rolls fire dataMappingModeDaysOffset tradeable days early, (2) the newly mapped contract has more than the offset remaining, (3) the streamed continuous series is genuinely back-adjusted vs. the raw mapped contract price.

### Run A — TradingDaysBeforeExpiry, offset 2 (full log attached)
runA_TradingDaysBeforeExpiry_clean.txt

Roll timing works. Mapping events fired on 2013-09-18 and 2013-12-18 — exactly 2 tradeable days earlier than the control's 09-20 / 12-20. Assertions (1) and (2) passed. The PR's core mapping mechanism is validated.

The series is not price-adjusted. Across 26,670 compared bars, max |adjusted/raw − 1| = 0.000624 — the continuous price equals the raw contract price on every bar:

2013-10-07 09:31:00 - adjusted continuous close: 1669.5, raw ES VMKLFZIH2MTD: 1669.375, ratio: 1.000075
2013-11-01 09:31:00 - adjusted continuous close: 1700, raw ES VMKLFZIH2MTD: 1699.625, ratio: 1.000221
2013-12-02 09:31:00 - adjusted continuous close: 1806.25, raw ES VMKLFZIH2MTD: 1806.375, ratio: 0.999931
Runtime Error: BackwardsRatio continuous series is NOT price adjusted: streamed continuous
price equals the raw mapped contract price (max deviation 0.000624). No factor rows exist
for DataMappingMode.TradingDaysBeforeExpiry, so MappingContractFactorProvider.GetPriceFactor
returned the default factor (1.0).

A second run reproduced the failure identically (same max deviation to six decimal places).

### Run B — control, LastTradingDay, same algorithm/data (full log attached)
runB_LastTradingDay_control_clean.txt

Completes cleanly. Ratios ~0.9346 on every sampled bar (max deviation 0.065880), matching the shipped Data/future/cme/factor_files/es.csv factors:

2013-10-07 09:31:00 - adjusted continuous close: 1560.27..., raw ES VMKLFZIH2MTD: 1669.375, ratio: 0.934647
2013-11-01 09:31:00 - adjusted continuous close: 1588.78..., raw ES VMKLFZIH2MTD: 1699.625, ratio: 0.934783
2013-12-02 09:31:00 - adjusted continuous close: 1688.07..., raw ES VMKLFZIH2MTD: 1806.375, ratio: 0.934512

### Scope of the bug

Notably, OrderListHash is identical between the two runs (1973b0beb9bc5e618e0387d960553d7a) — orders fill on raw contract prices, so trading mechanics are unaffected. The corruption is confined to the continuous data series: every indicator, signal, and statistic computed on it under the new mode would silently use unadjusted prices with roll gaps.

(Note: the 95% failed data requests in the DataMonitor section is expected with the sparse repo sample data and unrelated to the finding.)

Regression algorithm used

ContinuousFutureTradingDaysBeforeExpiryRegressionAlgorithm.cs

Once the normalization gap is fixed, this algorithm should pass under both modes; the
ExpectedStatistics/DataPoints placeholders can then be pinned and a Python twin added
to make it a standard regression test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configurable Rolling Date With DataMappingMode

2 participants